package she

import java.net.InetSocketAddress
import java.net.InetAddress
import java.nio.channels.ServerSocketChannel
import java.nio.channels.SocketChannel

import scala.actors.Actor
import scala.actors.Actor.actor
import scala.actors.Actor.loop
import scala.actors.Actor.react

import scala.collection.mutable

/** An HTTP server. When instantiated, a number of threads will be spawned
  * to deal with incoming connexions. However, the server must be bound to a
  * specific address before it can accept connexions.
  * A reporter must be mixed-in for logging purposes. */
class HttpServer { server: HttpServer with Reporter =>
  
  /** The time (in milieconds) the server will wait for a connexion to send the
    * first byte of a request after the connexion has been established or,
    * on persistent connexions, after a previous request has been answered. */
  var timeout: Long = 0L
  
  /** Functions that, for a given HTTP request, return a suitable HTTP
    * response, or 'None' if they don't know.
    * For performance reasons, it is important that 'None' is sent back
    * quickly if no response can be provided.
    * If a response can be provided, the execution time can be long (i.e. if the
    * server generates dynamic content, generating a response will typically involve
    * executing the application's business logic). This calculation will always
    * be done in an actor that is unique to the connexion. */
  type RequestHandler = HttpRequest => Option[HttpResponse]
  
  /** A queue of request handlers. Handlers in the queue will be executed
    * from top to bottom, until one handler returns a response for the
    * given request. If no handler accepts the request, an internal server
    * error (500) response will be used instead.
    * For performance reasons, this queue should be kept as short as possible. */
  val requestHandlers: mutable.Queue[RequestHandler] =
    new mutable.Queue
  
  /** Functions that, for a given Internet address, return whether they
    * know the address to be black-listed. */
  type Filter = InetAddress => Boolean
  
  /** A queue of address filters that are the server's black-list. Every connexion
    * will be tested against all filters in the queue and will be processed only if
    * no filter rejects it. It will be dropped otherwise. For performance reasons, this
    * queue should be kept very short. */
  val blackList: mutable.Queue[Filter] =
    new mutable.Queue
  
  /** Binds the server to a given address and port on the server. Until
    * binding is done, the server can't accept connections. After binding.
    * the server can accept connections only on the given port and
    * address. Binding may be requested multiple times and the server will
    * accept connections on all specified addresses and ports. */
  def bind(bindAddress: InetSocketAddress): Unit = {
    serverChannel.socket.bind(bindAddress)
    log(info, "Server bound to " + bindAddress.getHostName + ":" + bindAddress.getPort)
  }
  
  /** The server's connection socket. */
  protected val serverChannel: ServerSocketChannel =
    ServerSocketChannel.open
  
  /** An actor that accepts all connexion requests on the server and forwards
    * them to the filter actor. */
  protected val accept = actor {
    log(info, "Server started")
    while(serverChannel.isOpen)
      filter ! HttpConnexion(serverChannel.accept)
  }
  
  /** An actor that tests connexions against the server's black-list.
    * If the address of the client initiating the connexion is black-
    * listed, the connexion is dropped without reading any byte off
    * the channel, otherwise, the connexions is sent to the wait-for-
    * read actor. */
  protected val filter = actor {
    loop {
      react {
        case connexion: HttpConnexion =>
          val address = connexion.clientAddress
          if (blackList exists { reject => reject(address) }) {
            connexion.drop
            log(debug, "[drop] " + address.getHostAddress)
          }
          else
            waitForRead ! connexion
      }
    }
  }
  
  /** An actor that holds connexions until they provide fresh bytes to
    * read. Once new bytes are available from a connexions, it is
    * forwarded to the read actor.
    * TODO: connexions should be dropped if they stay longer than
    * 'timeout' */
  protected val waitForRead: Actor = actor {
    loop {
      react {
        case connexion: HttpConnexion =>
          if (connexion.bytesAvailable)
            read ! connexion
          else
            waitForRead ! connexion
      }
    }
  }
  
  /** An actor that reads bytes from a connexion and tries to parse them
    * as an HTTP request. There are three possible situations:
    * - A valid and complete HTTP request is parsed from the bytes that
    *   can be read without blocking. In this case, a new 'respond' actor
    *   is spawned to respond to the request.
    * - The bytes that can be read from the connexion do not form a
    *   prefix of any valid HTTP request. In this case, a new 'respond' actor
    *   is spawned to respond to the invalid request.
    * - The connexion runs-out of available bytes to read without blocking
    *   before any of the previous two situations can be determined. In this
    *   case, the connection, alongside the partially-built request,
    *   is sent back to the wait-for-read actor.  */
  protected val read = actor {
    loop {
      react {
        case connexion: HttpConnexion =>
          connexion.parseBytes match {
            case request: PartialRequest =>
              waitForRead ! connexion
            case request: FullRequest =>
              respond(request)
          }
      }
    }
  }
  
  /** Returns a new actor to answer the 'request'. This actor will look through 
    * the 'requestHandlers' queue from top to bottom until it finds a handler
    * that can provide a response for the request. It then sends this response
    * back to the client. If no handler can provide a response, it sends an
    * internal server error (500) response.
    * If the connexion is persistant (i.e. if the client can send multiple
    * requests on the same connexion), it will send the connexion back to the
    * wait-for-read actor. */
  protected def respond(request: HttpRequest) = actor {
    def findResponse(request: HttpRequest, handlers: List[RequestHandler]): HttpResponse = {
      handlers match {
        case hdl :: hdls =>
          hdl(request) match {
            case None =>
              findResponse(request, hdls)
            case Some(request) =>
              request
          }
        case Nil =>
          InternalServerError()
      }
    }
    val response = findResponse(request, requestHandlers.toList)
    response.send
    if (response.persist)
      waitForRead ! request.connexion
    else
      request.connexion.drop
  }
  
}
